Skip to content

Refresh Button for Map Positions with Notification Pop-Ups#506

Open
ZackLyon wants to merge 8 commits intodevelopfrom
feature/EDN-321-refresh-map-positions
Open

Refresh Button for Map Positions with Notification Pop-Ups#506
ZackLyon wants to merge 8 commits intodevelopfrom
feature/EDN-321-refresh-map-positions

Conversation

@ZackLyon
Copy link
Collaborator

@ZackLyon ZackLyon commented Feb 5, 2026

  • Refresh button on maps

    • Added a reusable RefreshButton in packages/react-ui that matches existing map control styling.
    • Button shows loading state and an optional tooltip with “last reload” and “next auto-refresh” text.
    • Map component accepts new props (onRequestRefresh, refreshLoading, refreshLastRefreshed, etc.) and renders the refresh button next to the existing buttons not the map
  • useRefreshPositions hook

    • New hook in apps/lrauv-dash2/lib/useRefreshPositions.tsx that:
      • Refetches vehicle position queries (via React Query) for the given vehicle names.
      • Tracks loading and lastRefreshed, and exposes markInitialLoadDone so the “next refresh” countdown can start after the first position load.
      • Shows toast notifications (bottom-left) after refresh with per-vehicle counts: GPS fixes, Argo, RWP, and emergencies (emergencies in red; emergency toasts do not auto-dismiss).
      • Throttles refresh so repeated clicks within ~3 seconds are ignored.
      • Optional 10-minute auto-refresh.
    • Uses existing vehiclePos query keys so refresh updates the same data the map/paths use.
  • formatElapsedTime helper

    • New utility in packages/utils to format a duration in ms as a short string (e.g. 45s, 23m, 5h:30m, 2d:12h, 6M, 2y).
    • Used in the refresh button tooltip and in the vehicle path tooltip for “time since latest fix”.
  • Deployment map and overview map

    • Both DeploymentMap and the overview map on the index page use useRefreshPositions and pass refreshAll, refreshLoading, lastRefreshed, and related props into the shared Map component so the new refresh button appears and works on both maps.
    • Deployment map uses a single-vehicle list when a vehicle is selected; overview map uses all tracked vehicles.
    • Optional preferredParams (e.g. deployment time range) can be passed so the hook aligns with the map’s time window.
  • VehiclePath and “first load” timing

    • VehiclePath now accepts an onPositionDataLoaded callback and calls it once when position data (e.g. GPS fixes) is first available.
    • Deployment and overview maps pass markInitialLoadDone so the refresh tooltip’s “last reload” / “next auto-refresh” countdown starts when the path data has loaded (with a 30s fallback if no path ever loads).
  • Default “from” for position queries

    • When deployment start/last-event time is missing, position queries now use a default from of 24 hours ago instead of 0.
    • Applied in both VehicleList and VehiclePath so the map can show recent positions even without deployment bounds.
  • Vehicle path tooltip

    • The path tooltip that shows the time of the current indicator and “time since latest fix” now uses formatElapsedTime for the “time since” part so it matches the refresh button’s compact format.
  • Styling

    • packages/react-ui/src/css/base.css updated with styles for the refresh button and the refresh-position toast.
  • Exports

    • formatElapsedTime is exported from @mbari/utils.

Refresh button

Screenshot 2026-02-04 at 4 30 25 PM

NOTE: In dash4, the map only shows the positions of the vehicle for the last 24 hours, whereas, dash5 shows either the positions since the beginning of an active deployment or it shows the entirety of the vehicle path for a recent previous deployment. The notifications for each reflect what is shown on the map (ie the number of GPS fixes will be much higher on dash5 since it is showing more positions).

@ZackLyon ZackLyon requested a review from jimjeffers February 5, 2026 00:34
@ZackLyon
Copy link
Collaborator Author

ZackLyon commented Feb 5, 2026

@jimjeffers - This is ready for your review. Please read the note to make sure you don't disagree with how this was implemented (follows current deployment map implementation).

@carueda
Copy link
Member

carueda commented Feb 18, 2026

Re:

In dash4, the map only shows the positions of the vehicle for the last 24 hours, ...

Just to clarify that this is actually user configurable in dash4: there's a maximum number of GPS positions to display setting as well as the length of the time window.

cc @heavyB @ksalamy

image

@jimjeffers
Copy link
Collaborator

Thanks for putting this together. The feature direction is strong, but I think these 4 issues should be addressed before merge.

1) 24h fallback is mostly inactive because query enabled still requires deployment timestamps

Files:

  • apps/lrauv-dash2/components/VehicleList.tsx
  • apps/lrauv-dash2/components/VehiclePath.tsx

Right now we compute a default from (24h ago), but the queries are still disabled unless deployment timestamps exist. That means the fallback path often doesn’t execute.

Example adjustment (pattern):

const defaultFrom = useMemo(() => Date.now() - 24 * 60 * 60 * 1000, [])
const queryFrom = lastDeployment?.lastEvent ?? defaultFrom

const { data: vehiclePosition } = useVehiclePos(
  { vehicle: name, from: queryFrom },
  { enabled: !!name }
)

In VehiclePath, use the same queryFrom approach and avoid enabled that depends on lastDeployment?.startEvent?.unixTime.

Tradeoff:

  • enabled: !!name may cause an extra request before deployment metadata resolves.
  • Gating on deployment metadata avoids that extra fetch but delays or suppresses data in “missing metadata” cases.
  • If the product requirement is “always show recent positions,” the immediate fallback is the better trade.

2) Emergency toast persistence doesn’t match the intent

File:

  • apps/lrauv-dash2/lib/useRefreshPositions.tsx

Current logic sets timeout = 0 for emergencies but then uses duration: timeout || undefined, which converts 0 to undefined and falls back to default behavior.

Example adjustment:

const duration = hasEmergencies ? Infinity : REFRESH_TOAST_DURATION_MS

toast(message, {
  id: getRefreshToastId(vehicleName),
  position: 'bottom-left',
  duration,
  className: 'refresh-position-toast',
})

Tradeoff:

  • Persistent emergency toasts improve visibility for critical events.
  • They can clutter the UI if multiple vehicles alert.
  • Good compromise: persistent + explicit dismiss, or long finite duration for emergencies.

3) refreshAll rethrows after showing error toast, which can surface unhandled promise rejections in click handlers

Files:

  • apps/lrauv-dash2/lib/useRefreshPositions.tsx
  • Call sites in map components (onRequestRefresh={refreshAll})

refreshAll handles UI error reporting already, then rethrows. Since it is used as a button click callback, this can produce noisy unhandled promise rejections.

Example adjustment A (UI-safe default):

catch (error) {
  logger.error('Error refreshing positions:', error)
  toast.error('Failed to refresh vehicle positions', { position: 'bottom-left' })
  return undefined
}

Example adjustment B (keep throw semantics but consume at callsite):

onRequestRefresh={() => {
  void refreshAll().catch(() => {})
}}

Tradeoff:

  • No-throw behavior is cleaner for UI handlers.
  • Throwing is useful if callers/tests need strict failure propagation.
  • Best long-term API is often two functions: a UI-safe refreshAll and a strict refreshAllOrThrow.

4) Auto-refresh timing is coupled to a callback with unstable identity

File:

  • apps/lrauv-dash2/lib/useRefreshPositions.tsx

The auto-refresh effect depends on refreshAll; if refreshAll is recreated frequently (due to non-memoized array/object inputs), timers reset and scheduling can drift/miss expected execution.

Example adjustment:

const refreshRef = useRef(refreshAll)
useEffect(() => {
  refreshRef.current = refreshAll
}, [refreshAll])

useEffect(() => {
  if (!autoRefreshMinutes || !lastRefreshed) return

  const nextAt = lastRefreshed.getTime() + autoRefreshMinutes * 60_000
  const delay = nextAt - Date.now()

  const run = () => {
    void refreshRef.current()
  }

  if (delay <= 0) {
    run()
    return
  }

  const id = setTimeout(run, delay)
  return () => clearTimeout(id)
}, [autoRefreshMinutes, lastRefreshed])

Also stabilize inputs at callsites with useMemo for vehicleNames and preferredParams so callback churn is reduced.

Tradeoff:

  • Ref+effect scheduling is slightly more code, but more reliable.
  • setTimeout is efficient/precise for “next run” scheduling.
  • setInterval is simpler but can drift and do unnecessary work.

If helpful, I can put up a small follow-up PR with these fixes directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants